/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.common.util;

import java.io.IOException;
import java.io.Writer;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAccumulator;
import java.util.concurrent.atomic.LongAdder;
import org.apache.solr.client.api.util.ReflectWritable;
import org.apache.solr.common.EnumFieldValue;
import org.apache.solr.common.IteratorWriter;
import org.apache.solr.common.MapSerializable;
import org.apache.solr.common.MapWriter;
import org.apache.solr.common.PushWriter;

/** Base interface for all text based writers */
public interface TextWriter extends PushWriter {

  default void writeVal(String name, Object val) throws IOException {
    writeVal(name, val, false);
  }

  default void writeVal(String name, Object val, boolean raw) throws IOException {

    // if there get to be enough types, perhaps hashing on the type
    // to get a handler might be faster (but types must be exact to do that...)
    //    (see a patch on LUCENE-3041 for inspiration)

    // go in order of most common to least common, however some of the more general types like Map
    // belong towards the end
    if (val == null) {
      writeNull(name);
    } else if (val instanceof CharSequence) {
      if (raw) {
        writeStrRaw(name, val.toString());
      } else {
        writeStr(name, val.toString(), true);
        // micro-optimization... using toString() avoids a cast first
      }
    } else if (val instanceof Number) {
      writeNumber(name, (Number) val);
    } else if (val instanceof Boolean) {
      writeBool(name, (Boolean) val);
    } else if (val instanceof AtomicBoolean) {
      writeBool(name, ((AtomicBoolean) val).get());
    } else if (val instanceof Date) {
      writeDate(name, (Date) val);
    } else if (val instanceof NamedList) {
      writeNamedList(name, (NamedList) val);
    } else if (val instanceof Path) {
      final String pathStr = ((Path) val).toAbsolutePath().toString();
      if (raw) {
        writeStrRaw(name, pathStr);
      } else {
        writeStr(name, pathStr, true);
      }
    } else if (val instanceof IteratorWriter) {
      writeIterator(name, (IteratorWriter) val, raw);
    } else if (val instanceof MapWriter) {
      writeMap(name, (MapWriter) val);
    } else if (val instanceof ReflectWritable) {
      writeVal(name, Utils.getReflectWriter(val));
    } else if (val instanceof MapSerializable) {
      // todo find a better way to reuse the map more efficiently
      writeMap(name, ((MapSerializable) val).toMap(new LinkedHashMap<>()), false, true);
    } else if (val instanceof Map) {
      writeMap(name, (Map) val, false, true);
    } else if (val instanceof Collection<?> cval) { // very generic; keep towards the end
      writeArray(name, cval.iterator(), cval.size(), raw);
    } else if (val instanceof Iterator) { // very generic; keep towards the end
      writeArray(name, (Iterator) val, raw);
    } else if (val instanceof Iterable) { // very generic; keep towards the end
      writeArray(name, ((Iterable) val).iterator(), raw);
    } else if (val instanceof Object[]) {
      writeArray(name, (Object[]) val, raw);
    } else if (val instanceof byte[] arr) {
      writeByteArr(name, arr, 0, arr.length);
    } else if (val instanceof EnumFieldValue) {
      if (raw) {
        writeStrRaw(name, val.toString());
      } else {
        writeStr(name, val.toString(), true);
      }
    } else {
      // Fallback to do *something*, either use a reflection writer or write as a string
      // representation.
      writeVal(name, Utils.getReflectWriter(val));
    }
  }

  /**
   * Writes the specified val directly to the backing writer, without wrapping (e.g., in quotes) or
   * escaping of any kind.
   */
  default void writeStrRaw(String name, String val) throws IOException {
    throw new UnsupportedOperationException();
  }

  void writeStr(String name, String val, boolean needsEscaping) throws IOException;

  void writeMap(String name, Map<?, ?> val, boolean excludeOuter, boolean isFirstVal)
      throws IOException;

  void writeArray(String name, Iterator<?> val, boolean raw) throws IOException;

  void writeArray(String name, Iterator<?> val, int size, boolean raw) throws IOException;

  void writeNull(String name) throws IOException;

  /** if this form of the method is called, val is the Java string form of an int */
  void writeInt(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Java string form of a long */
  void writeLong(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Java string form of a boolean */
  void writeBool(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Java string form of a float */
  void writeFloat(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Java string form of a double */
  void writeDouble(String name, String val) throws IOException;

  /** if this form of the method is called, val is the Solr ISO8601 based date format */
  void writeDate(String name, String val) throws IOException;

  void writeNamedList(String name, NamedList<?> val) throws IOException;

  Writer getWriter();

  default void writeNumber(String name, Number val) throws IOException {
    if (val instanceof Integer) {
      writeInt(name, val.toString());
    } else if (val instanceof Long) {
      writeLong(name, val.toString());
    } else if (val instanceof Float) {
      // we pass the float instead of using toString() because
      // it may need special formatting. same for double.
      writeFloat(name, val.floatValue());
    } else if (val instanceof Double) {
      writeDouble(name, val.doubleValue());
    } else if (val instanceof Short) {
      writeInt(name, val.toString());
    } else if (val instanceof Byte) {
      writeInt(name, val.toString());
    } else if (val instanceof AtomicInteger) {
      writeInt(name, ((AtomicInteger) val).get());
    } else if (val instanceof AtomicLong) {
      writeLong(name, ((AtomicLong) val).get());
    } else if (val instanceof LongAdder) {
      writeLong(name, val.toString());
    } else if (val instanceof LongAccumulator) {
      writeLong(name, val.toString());
    } else {
      // default... for debugging only
      writeStr(name, val.getClass().getName() + ':' + val.toString(), true);
    }
  }

  default void writeArray(String name, Object[] val, boolean raw) throws IOException {
    writeArray(name, Arrays.asList(val), raw);
  }

  default void writeArray(String name, List<?> l, boolean raw) throws IOException {
    writeArray(name, l.iterator(), raw);
  }

  default void writeDate(String name, Date val) throws IOException {
    writeDate(name, val.toInstant().toString());
  }

  default void writeByteArr(String name, byte[] buf, int offset, int len) throws IOException {
    writeStr(
        name,
        new String(
            Base64.getEncoder().encode(ByteBuffer.wrap(buf, offset, len)).array(),
            StandardCharsets.ISO_8859_1),
        false);
  }

  default void writeInt(String name, int val) throws IOException {
    writeInt(name, Integer.toString(val));
  }

  default void writeLong(String name, long val) throws IOException {
    writeLong(name, Long.toString(val));
  }

  default void writeBool(String name, boolean val) throws IOException {
    writeBool(name, Boolean.toString(val));
  }

  default void writeFloat(String name, float val) throws IOException {
    String s = Float.toString(val);
    // If it's not a normal number, write the value as a string instead.
    // The following test also handles NaN since comparisons are always false.
    if (val > Float.NEGATIVE_INFINITY && val < Float.POSITIVE_INFINITY) {
      writeFloat(name, s);
    } else {
      writeStr(name, s, false);
    }
  }

  default void writeDouble(String name, double val) throws IOException {
    String s = Double.toString(val);
    // If it's not a normal number, write the value as a string instead.
    // The following test also handles NaN since comparisons are always false.
    if (val > Double.NEGATIVE_INFINITY && val < Double.POSITIVE_INFINITY) {
      writeDouble(name, s);
    } else {
      writeStr(name, s, false);
    }
  }

  default void writeBool(String name, Boolean val) throws IOException {
    writeBool(name, val.toString());
  }

  @Override
  default void writeMap(MapWriter mw) throws IOException {
    // todo
  }

  default void writeMap(String name, MapWriter mw) throws IOException {
    writeMap(mw);
  }

  @Override
  default void writeIterator(IteratorWriter iw) throws IOException {
    /*todo*/
  }

  default void writeIterator(String name, IteratorWriter iw, boolean raw) throws IOException {
    writeIterator(iw);
  }

  default void indent() throws IOException {
    if (doIndent()) indent(level());
  }

  int incLevel();

  int decLevel();

  TextWriter setIndent(boolean doIndent);

  int level();

  boolean doIndent();

  default void indent(int lev) throws IOException {
    getWriter()
        .write(
            SolrJSONWriter.indentChars,
            0,
            Math.min((lev << 1) + 1, SolrJSONWriter.indentChars.length));
  }
}
